Jerry's Log

Logging in JAVA

contents

자바에서 로그를 남기는 건 역사적인 이유로 인해 수많은 라이브러리(Log4j, Logback, SLF4J, JUL, JCL 등)가 존재하기 때문에 처음 접하면 매우 헷갈립니다.

스프링에서의 로깅을 이해하려면 먼저 "퍼사드 패턴(Facade Pattern)" 을 이해해야 합니다.

다음은 자바 로깅 아키텍처와 스프링 부트에서의 설정 방법입니다.


1. 아키텍처: 인터페이스(Interface) vs 구현체(Implementation)

자바에서는 "API"(코드에 작성하는 것)"엔진"(실제로 로그를 기록하는 것) 을 분리합니다.

A. 퍼사드 (인터페이스)

여러분은 항상 퍼사드(Facade) 를 보고 코딩해야 합니다. 그래야 나중에 자바 코드를 수정하지 않고도 로깅 라이브러리(엔진)만 교체할 수 있습니다.

B. 구현체 (엔진)

실제로 파일 쓰기(IO) 작업을 수행하는 라이브러리입니다.


2. 코드에서 사용하는 법 (Lombok)

현대적인 스프링 부트 개발에서는 로거를 수동으로 생성하는 일이 드뭅니다. Lombok을 사용하세요.

수동 방식 (보일러플레이트 코드):

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PaymentService {
    // 매번 이 줄을 작성해야 함
    private static final Logger log = LoggerFactory.getLogger(PaymentService.class);

    public void pay() {
        log.info("결제 처리 중");
    }
}

Lombok 방식 (권장):

import lombok.extern.slf4j.Slf4j;

@Slf4j // 자동으로 'log' 필드를 생성해 줌
@Service
public class PaymentService {
    public void pay() {
        log.info("결제 처리 중");
    }
}

3. 로그 레벨 (계층 구조)

상용 환경 디버깅을 위해 레벨 이해는 필수입니다. 레벨을 INFO로 설정하면, 그보다 낮은 레벨의 로그는 전부 무시됩니다.

  1. TRACE: 극도로 상세함 (예: 루프 변수 값). "for문 인덱스 i=4".
  2. DEBUG: 개발자용 정보 (예: 페이로드 내용). "User 객체 데이터: {name: '철수'}".
  3. INFO: 정상적인 애플리케이션 흐름 (기본값). "애플리케이션 시작됨", "사용자 로그인".
  4. WARN: 잠재적 문제지만 앱은 계속 동작함. "디스크 공간 90% 참", "DB 연결 재시도 중".
  5. ERROR: 뭔가 고장 남. "NullPointerException 발생", "DB 연결 불가".

설정 (application.yml):

logging:
  level:
    root: INFO # 전체 기본값
    com.example.myproject: DEBUG # 내 코드만 DEBUG 레벨로 자세히 보기
    org.springframework.web: WARN # 스프링 내부 로그는 시끄러우니까 줄이기

4. Logback 설정하기 (logback-spring.xml)

application.yml은 간단한 설정에는 좋지만, 상용 환경에서는 로그 회전(Rolling) 전략(디스크가 꽉 차지 않게 매일 새 파일 생성)과 커스텀 패턴이 필요합니다. 이를 위해 src/main/resources/logback-spring.xml 파일을 만듭니다.

설정 핵심 개념:

  1. Appender: 로그를 어디에 출력할지 (콘솔, 파일, 소켓, DB 등).
  2. Encoder/Pattern: 로그를 어떤 모양으로 찍을지.
  3. Policy: 언제 새 파일을 만들지 (시간 기준, 용량 기준).

logback-spring.xml 예시:


    
        
            %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
        
    

    
        logs/app.log
        
        
            logs/app-%d{yyyy-MM-dd}.%i.log
            10MB
            30 
        
        
            %d %-5level [%X{requestId}] %logger{35} - %msg%n
        
    

    
        
            
        
    

    
        
            
        
    

5. 심화: MDC (Mapped Diagnostic Context)

멀티 스레드 웹 서버에서 필수적인 기술입니다.

여러 사용자가 동시에 서버에 요청을 보내면, 로그 파일에 여러 사용자의 로그가 뒤섞이게 됩니다. 특정 사용자의 요청 하나만 추적하려면 어떻게 해야 할까요?

MDCThreadLocal을 사용하는 맵(Map)과 같습니다. 요청이 시작될 때 "요청 ID"를 넣어두면, 해당 스레드에서 찍히는 모든 로그에 자동으로 그 ID가 포함됩니다.

인터셉터(Interceptor) 예시:

public class LogInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String uuid = UUID.randomUUID().toString();
        MDC.put("requestId", uuid); // ThreadLocal에 저장
        return true;
    }

    @Override
    public void afterCompletion(...) {
        MDC.clear(); // 중요: 메모리 누수 방지를 위해 반드시 비워야 함!
    }
}

로그 출력 결과:

2023-10-01 INFO [a1b2-c3d4] PaymentService - 처리 중...
2023-10-01 INFO [a1b2-c3d4] Database - 저장 중...

이제 grep a1b2-c3d4 명령어로 해당 사용자의 여정만 쏙 뽑아볼 수 있습니다.

6. 베스트 프랙티스

  1. PII(개인식별정보) 로깅 금지: 비밀번호, 카드 번호, 주민등록번호는 절대 로그에 남기면 안 됩니다.
  2. System.out.println 절대 금지: 레벨 제어도 안 되고, 타임스탬프도 없고, 성능상 스레드 락(Lock)을 유발할 수 있습니다. 무조건 log.info()를 쓰세요.
  3. Placeholders({}) 사용:
    • ❌ 나쁨: log.debug("User " + user.getName() + " logged in"); (DEBUG가 꺼져 있어도 문자열 더하기 연산이 실행됨).
    • ✅ 좋음: log.debug("User {} logged in", user.getName()); (DEBUG가 켜져 있을 때만 연산 수행).
  4. 비동기 로깅 (Async Logging): 트래픽이 매우 많은 시스템이라면, 디스크 쓰기 작업이 스레드를 멈추게(Blocking) 할 수 있습니다. Log4j2 Async Appender를 사용하여 별도의 스레드에서 로그를 쓰게 하면 사용자 응답 속도에 영향을 주지 않습니다.

references